﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.Linq;
using System.Text;
using SlamCms.Common;
using SlamCms.Services;

namespace SlamCms.Data
{
	public class Database
	{
		private readonly DataContext _dataContext; 
		private TimeSpan _lastCommandExecutionTime;

		internal Database(DataContext dataContext)
		{
			Ensure.ArgumentNotNull(dataContext, "dataContext");
			_dataContext = dataContext;
		}

		public TimeSpan LastCommandExecutionTime
		{
			get
			{
				return _lastCommandExecutionTime;
			}
		}

		public void ExecuteReader(string sql, Action<IDataReader> action)
		{
			Ensure.ArgumentNotNullOrEmpty(sql, "sql");
			Ensure.ArgumentNotNull(action, "action");

			DoWithConnection(sql, null, connection =>
			{
				using (var cmd = connection.CreateCommand())
				{
					cmd.CommandText = sql;
					// TODO support parameters

					using (var reader = cmd.ExecuteReader())
					{
						action(reader);
					}
				}                 
			});
		}

		public IEnumerable<T> ExecuteReader<T>(string sql, object param = null)
		{
			Ensure.ArgumentNotNullOrEmpty(sql, "sql");

			IEnumerable<T> result = new List<T>();

			DoWithConnection(sql, param, connection =>
			{
				result = connection.Query<T>(sql, param ?? new { });
			});

			return result;
		}

		public T? ExecuteScalar<T>(string sql, object param = null) where T : struct
		{ 
			Ensure.ArgumentNotNullOrEmpty(sql, "sql");

			T? result = null;

			DoWithConnection(sql, param, connection =>
			{
				result = connection.Query<T>(sql, param ?? new { }).FirstOrDefault();
			});

			return result;
		}

		public int ExecuteNonQuery(string sql, object param = null)
		{
			Ensure.ArgumentNotNullOrEmpty(sql, "sql");

			int result = 0;

			DoWithConnection(sql, param, connection =>
			{
				result = connection.Execute(sql, param ?? new { });
			});

			return result;
		}

		[Obsolete("Please use Query")]
		public IEnumerable<T> Retrieve<T>(string sql)
		{
			return Query<T>(sql);
		}

		public IEnumerable<T> RetrieveAll<T>(string tableName = null, string orderBy = "") where T : class, new()
		{
			if (tableName == null)
				tableName = typeof(T).Name;

			if (tableName.IsNullOrEmpty())
				throw new ArgumentException("The table name is not valid. If you pass null the method will use the generic type name as table name.", "tableName");

			tableName = tableName.Trim();

			if (!tableName.StartsWith("["))
				tableName = "[{0}]".F(tableName);

			var sb = new StringBuilder();
			sb.Append(@"select * from {0} with (nolock)".F(tableName));
			if (!orderBy.IsNullOrEmpty())
				sb.Append(@" order by {0}".F(orderBy));

			return this.Query<T>(sb.ToString());
		}

		public IEnumerable<T> Query<T>(string sql, object param = null)
		{
			IEnumerable<T> result = null;
			
			DoWithConnection(sql, param, connection =>
			{
				result = connection.Query<T>(sql, param ?? new {});
			});

			return result;
		}

		protected void DoWithConnection(string sql, object param, Action<IDbConnection> action, Action<DbException> onError = null)
		{
			Ensure.ArgumentNotNull(action, "action");

			var connection = GetConnection();

			var stopwatch = Stopwatch.StartNew();

			using (connection.EnsureOpen())
			{
				try
				{
					action(connection);
					stopwatch.Stop();
				}
				catch (DbException ex)
				{
					stopwatch.Stop();
					ex.Data.Add("SQL", sql);
					ex.Data.Add("SQLParam", param);

					if (onError != null)
						onError(ex);
					else
						throw;
				}
				finally
				{
					_lastCommandExecutionTime = stopwatch.Elapsed;
				}
			}
		}

		protected IDbConnection GetConnection()
		{
			var connection = _dataContext.Configuration.DependencyResolver.GetService<IDbConnection>();
			if (connection == null)
			{
				var dbConnectionResolver = this._dataContext.Configuration.Services.GetService<IDbConnectionResolver>();
				if (dbConnectionResolver == null)
					throw new Exception("No database connection factory was specified. Try making the Dependency Resolver to return an IDbConnection or IDbConnectionResolver");

				connection = dbConnectionResolver.GetConnection();
				if (connection == null)
					throw new Exception("Could not retrieve a connection from the registered {0}".F(dbConnectionResolver.GetType().Name));
			}

			return connection;
		}
	}
}
